#include "jntpsocket.h"
#include <QTECore/jlog.h>
#include <QTimerEvent>

#include <QHostAddress>
#include <QUdpSocket>
#include <QDateTime>
#include <QHostInfo>

namespace QTE
{

typedef JNTPSocket::Format  Format;

class Q_DECL_HIDDEN JNTPSocketPrivate : public QObject
{
	Q_OBJECT

public:
	explicit JNTPSocketPrivate(JNTPSocket *q_ptr);
	bool request(const JNTPSocket::Format& format);

public slots:
	void on_lookupHost(const QHostInfo &info);
	void readDatagram(bool emitSignal);

protected:
	void timerEvent(QTimerEvent *event) override;

public:
	JNTPSocket *q_ptr;
	QUdpSocket *m_socket;
	Format m_format;

	QString m_hostName;
	int m_currentLookupHostID = -1;

	QHostAddress m_address { "52.231.114.183" };
	quint16 m_port = 123;

	QDateTime m_buffer;
	int m_timerID = -1;

	bool m_DNSFinished = false;
	QList<JNTPSocket::Format> m_requestCounter;
};

JNTPSocketPrivate::JNTPSocketPrivate(JNTPSocket *q_ptr) :
	QObject(q_ptr),
	q_ptr(q_ptr),
	m_socket(new QUdpSocket(this))
{
	connect(m_socket, &QUdpSocket::readyRead, this, [this](){readDatagram(true);});

	m_currentLookupHostID =
			QHostInfo::lookupHost("time.windows.com", this, SLOT(on_lookupHost(QHostInfo)));
}

bool JNTPSocketPrivate::request(const JNTPSocket::Format &format)
{
#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
	auto epoch = QDate(1900, 1, 1).startOfDay();
#else
	QDateTime epoch(QDate(1900, 1, 1));
#endif

	quint32 second = epoch.secsTo(QDateTime::currentDateTime());
	char buf[48] = {};

	buf[0] = format.LI << 6 | format.VN << 3 | format.MODE;
	buf[1] = format.STRATUM;
	buf[2] = format.POLL;
	buf[3] = format.PREC & 0xFF;
	buf[5] = buf[9] = 1;

	//40到43字节保存原始时间戳，即客户端发送的时间
	buf[40] = (second & 0xFF000000) >> 24;
	buf[41] = (second & 0x00FF0000) >> 16;
	buf[42] = (second & 0x0000FF00) >>  8;
	buf[43] = (second & 0x000000FF) >>  0;

	if( m_socket->writeDatagram(buf, sizeof(buf), m_address, m_port) != sizeof(buf) )
		return false;

	return m_socket->flush();
}

void JNTPSocketPrivate::on_lookupHost(const QHostInfo &info)
{
	m_currentLookupHostID = -1;
	m_DNSFinished = true;
	auto addrs = info.addresses();

	if( not addrs.isEmpty() )
	{
		m_address = addrs[0];
		for(auto &format : m_requestCounter)
			request(format);
	}
	m_requestCounter.clear();
}

void JNTPSocketPrivate::readDatagram(bool emitSignal)
{
	char buf[48] = {};
	QHostAddress address;
	quint16 port;

	qint64 res = m_socket->readDatagram(buf, sizeof(buf), &address, &port);

	if( res < 0 )
	{
		auto errmsg = QString("*** Warning: QTE::JNTPSocket: read datagram error. (%1:%2): %3")
					  .arg(m_address.toString()).arg(m_port).arg(m_socket->errorString()).toStdString();
		jWarning() << errmsg.c_str();
		return ;
	}

	else if( res == 0 )
		return ;

	QByteArray TransmitTimeStamp = QByteArray(buf, sizeof(buf)).right(8);

	quint32 seconds = TransmitTimeStamp[0];
	quint32 mseconds = TransmitTimeStamp[4];

	int j = 1;

	for(; j<4; j++)
	{
		seconds <<= 8;
		seconds += J_SCT(quint8, TransmitTimeStamp[j]);
	}

	for(++j; j<8; j++)
	{
		mseconds <<= 8;
		mseconds += J_SCT(quint8, TransmitTimeStamp[j]);
	}

	mseconds = ((mseconds >> 12) - 759 * (((mseconds >> 10) + 32768) >> 16)) / 1000;

#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
	auto epoch = QDate(1900, 1, 1).startOfDay();
	auto unixStart = QDate(1970, 1, 1).startOfDay();
#else
	QDateTime epoch(QDate(1900, 1, 1));
	QDateTime unixStart(QDate(1970, 1, 1));
#endif

	m_buffer.setMSecsSinceEpoch(seconds * 1000ULL + mseconds - epoch.msecsTo(unixStart));

	if( emitSignal )
		emit q_ptr->received(m_buffer);
}

void JNTPSocketPrivate::timerEvent(QTimerEvent *event)
{
	QObject::timerEvent(event);

	if( event->timerId() == m_timerID )
		q_ptr->request(m_format);
}

/*----------------------------------------------------------------------------------*/

JNTPSocket::JNTPSocket(QObject *parent) :
	QObject(parent),
	d_ptr(new JNTPSocketPrivate(this))
{

}

JNTPSocket::JNTPSocket(const QHostAddress &address, quint16 port, QObject *parent) :
	JNTPSocket(parent)
{
	setServer(address, port);
}

JNTPSocket::JNTPSocket(const QString &hostName, quint16 port, QObject *parent) :
	JNTPSocket(parent)
{
	setServer(hostName, port);
}

JNTPSocket::~JNTPSocket()
{

}

bool JNTPSocket::request(const Format &format)
{
	if( not d_ptr->m_DNSFinished )
	{
		d_ptr->m_requestCounter << format;
		return true;
	}
	else if( d_ptr->m_address.isNull() )
	{
		jWarning() << "The requested host address is invalid.";
		return false;
	}
	return d_ptr->request(format);
}

bool JNTPSocket::setServer(const QHostAddress &address, quint16 port)
{
	if( address.isNull() )
	{
		jWarning() << "Host address is invalid.";
		return false;
	}

	d_ptr->m_DNSFinished = true;
	d_ptr->m_hostName = "";

	d_ptr->m_address = address;
	d_ptr->m_port = port;
	return true;
}

bool JNTPSocket::setServer(const QString &hostName, quint16 port)
{
	if( hostName.isEmpty() )
	{
		jWarning() << "Host ip or domain name is invalid. (empty)";
		return false;
	}

	d_ptr->m_port = port;
	if( d_ptr->m_address.setAddress(hostName) )
		return true;

	d_ptr->m_hostName = hostName;
	d_ptr->m_DNSFinished = false;

	if( d_ptr->m_currentLookupHostID != -1 )
		QHostInfo::abortHostLookup(d_ptr->m_currentLookupHostID);

	d_ptr->m_currentLookupHostID =
			QHostInfo::lookupHost(hostName, d_ptr, SLOT(on_lookupHost(QHostInfo)));
	return true;
}

void JNTPSocket::start(const Format &format, int msecs)
{
	if( d_ptr->m_timerID <= 0 )
	{
		d_ptr->m_format = format;
		d_ptr->m_timerID = d_ptr->startTimer(msecs);
		request(d_ptr->m_format);
	}
}

void JNTPSocket::stop()
{
	if( d_ptr->m_timerID > 0 )
	{
		d_ptr->killTimer(d_ptr->m_timerID);
		d_ptr->m_timerID = -1;
	}
}

QHostAddress JNTPSocket::serverAddress() const
{
	return d_ptr->m_address;
}

quint16 JNTPSocket::serverPort() const
{
	return d_ptr->m_port;
}

bool JNTPSocket::isStart() const
{
	return d_ptr->m_timerID > 0;
}

QDateTime JNTPSocket::waitForReceived(int msecs)
{
	if( d_ptr->m_currentLookupHostID != -1 )
		QHostInfo::abortHostLookup(d_ptr->m_currentLookupHostID);

	d_ptr->on_lookupHost(QHostInfo::fromName(d_ptr->m_hostName));
	if( d_ptr->m_hostName.isNull() )
		return QDateTime();

	d_ptr->m_socket->waitForReadyRead(msecs);
	d_ptr->readDatagram(false);
	return d_ptr->m_buffer;
}

QString JNTPSocket::errorString() const
{
	if( d_ptr->m_DNSFinished and d_ptr->m_address.isNull() )
		return "Domain name resolution failure.";
	return d_ptr->m_socket->errorString();
}

} //namespace QTE

#include <moc_jntpsocket.cpp>
#include <jntpsocket.moc>
